#include "Containers/AxisBinning.h"
#include "Containers/TokenizedLine.h"
#include "Functions/Error.h"
#include "Functions/TerminalIO.h"
#include "TList.h"
#include "TString.h"
#include <cmath>
#include <sstream>
using namespace std;
using namespace BOSS_Afterburner::TerminalIO;

AxisBinning::AxisBinning(const TString& input) :
  fHasBinning(false),
  fNBins(0),
  fBinWidth(0.),
  fRange(0., 0.)
{
  SetBinning(input);
}

AxisBinning::AxisBinning(const Int_t& nbins, const Double_t& from, const Double_t& to) :
  fHasBinning(false),
  fNBins(0),
  fBinWidth(0.),
  fRange(0., 0.)
{
  SetBinning(nbins, from, to);
}

AxisBinning::AxisBinning(const Double_t& width, const Double_t& from, const Double_t& to) :
  fHasBinning(false),
  fNBins(0),
  fBinWidth(0.),
  fRange(0., 0.)
{
  SetBinning(width, from, to);
}

void AxisBinning::SetBinning(const TString& input)
{
  TokenizedLine tokenizer{input};
  if(!tokenizer.GetNSegments() == 3)
    EXCEPTION(Form("Binning string \"%s\" has to contain 2 commas", input.Data()));
  Int_t    nbins{0};
  Double_t width{0.};
  auto&    arg1 = tokenizer.GetSegment(0);
  if(arg1.Contains('.') || arg1.Contains('e') || arg1.Contains('E'))
    width = arg1.Atof();
  else
    nbins = arg1.Atoi();
  auto from = tokenizer.GetSegment(1).Atof();
  auto to   = tokenizer.GetSegment(2).Atof();
  if(nbins)
    SetBinning(nbins, from, to);
  else
    SetBinning(width, from, to);
}

void AxisBinning::SetBinning(const YAML::Node& input)
{
  const auto& range    = input["range"];
  const auto& binWidth = input["bin width"];
  const auto& nBins    = input["n bins"];
  if(!range || (!binWidth && !nBins)) return;
  if(range.size() != 2) return;
  if(binWidth.IsDefined())
    SetBinning(binWidth.as<Double_t>(), range[0].as<Double_t>(), range[1].as<Double_t>());
  else if(nBins.IsDefined())
    SetBinning(nBins.as<Int_t>(), range[0].as<Double_t>(), range[1].as<Double_t>());
}

void AxisBinning::SetBinning(const Int_t& nbins, const Double_t& from, const Double_t& to)
{
  SetBinning(nbins, ComputeBinWidth(nbins, from, to), from, to);
}

void AxisBinning::SetBinning(const Double_t& width, const Double_t& from, const Double_t& to)
{
  SetBinning(ComputeNBins(width, from, to), width, from, to);
}

void AxisBinning::SetBinning(const Int_t& nbins, const Double_t& width, const Double_t& from,
                             const Double_t& to)
{
  if(!CheckBinWidth(width, from, to))
  {
    PrintWarning(
      Form("Problem with bin width %g: %d bins (from %g to %g)", width, nbins, from, to));
    return;
  }
  if(!CheckNBins(nbins))
  {
    PrintWarning(
      Form("Problem with %d bins: bin width %g (from %g to %g)", nbins, width, from, to));
    return;
  }
  fBinWidth   = width;
  fNBins      = nbins;
  fRange      = {from, to};
  fHasBinning = true;
}

void AxisBinning::SetBinWidth(const Double_t& width)
{
  SetBinning(ComputeNBins(width, fRange.first, fRange.second), width, fRange.first, fRange.second);
}

void AxisBinning::SetNBins(const Int_t& nbins)
{
  SetBinning(nbins, ComputeBinWidth(nbins, fRange.first, fRange.second), fRange.first,
             fRange.second);
}

void AxisBinning::SetRange(const Double_t& from, const Double_t& to)
{
  SetBinning(fNBins, fBinWidth, from, to);
}

const Double_t AxisBinning::ComputeRange(const Double_t& from, const Double_t& to) const
{
  return to - from;
}

const bool AxisBinning::CheckNBins(const Int_t& nbins) const
{
  return nbins < gMaxNBins && nbins > 0;
}

const bool AxisBinning::CheckBinWidth(const Double_t& width, const Double_t& from,
                                      const Double_t& to) const
{
  return width > 0 && ComputeNBins(width, from, to) < gMaxNBins;
}

const bool AxisBinning::CheckRange(const Double_t& from, const Double_t& to) const
{
  return from < to;
}

const Double_t AxisBinning::ComputeBinWidth(const Int_t& nbins, const Double_t& from,
                                            const Double_t& to) const
{
  return ComputeRange(from, to) / nbins;
}

const Int_t AxisBinning::ComputeNBins(const Double_t& width, const Double_t& from,
                                      const Double_t& to) const
{
  return ceil(ComputeRange(from, to) / width);
}

const Int_t& AxisBinning::NBins() const
{
  if(fHasBinning)
    return fNBins;
  else
    return 0;
}
const Double_t& AxisBinning::From() const
{
  if(fHasBinning)
    return fRange.first;
  else
    return 0.;
}
const Double_t& AxisBinning::To() const
{
  if(fHasBinning)
    return fRange.second;
  else
    return 0.;
}
const Double_t& AxisBinning::BinWidth() const
{
  if(fHasBinning)
    return fBinWidth;
  else
    return 0.;
}

const TString AxisBinning::BuildString() const
{
  return BuildString_AxisBinning();
}

const TString AxisBinning::BuildString_AxisBinning() const
{
  if(HasBinning())
    return Form("%d bins, from %g to %g (width %g)", NBins(), From(), To(), BinWidth());
  return "";
}